package gagak.framework.web.param;

import gagak.framework.web.reflect.AnnotationAttributes;
import gagak.framework.web.reflect.ParameterNameDiscoverer;
import gagak.framework.web.utils.Assert;
import gagak.framework.web.utils.ObjectUtils;
import com.lowagie.tools.Executable;
import com.sun.star.beans.Optional;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;

/*
 * @program: ce
 * @description:  对方法参数进行封装
 * @author: houhong
 * @create: 2022-11-23 21:30
 */
public class MethodParameter {

    private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0];

    /*
     * 参数所属方法
     */
    private Method method;
    /*
     * 形参名
     */
    private volatile String parameterName;

    /*
     * 参数索引
     */
    private final int parameterIndex;

    /*
     * 参数类型
     */
    private volatile Class<?> parameterType;

    /*
     * 参数基本信息
     */
    private volatile Parameter parameter;



    /*
     * 范型数据类型
     */
    private volatile Type genericParameterType;


    /*
     * 参数注解
     */
    private volatile Annotation[] parameterAnnotations;
    /*
     * 方法参数注解属性
     */
    private volatile List<AnnotationAttributes> annotationAttributes = new ArrayList<AnnotationAttributes>(8);


    private int nestingLevel;


    public Map<Integer, Integer> typeIndexesPerLevel;


    private volatile Class<?> containingClass;


    private volatile ParameterNameDiscoverer parameterNameDiscoverer;


    private volatile MethodParameter nestedMethodParameter;


    public MethodParameter(Method method, int parameterIndex) {
        this(method, parameterIndex, 1);
    }


    public MethodParameter(Method method, int parameterIndex, int nestingLevel) {
        Assert.notNull(method, "Method must not be null");
        this.method = method;
        this.parameterIndex = parameterIndex;
        // this.parameterIndex = validateIndex(null, parameterIndex);
        this.nestingLevel = nestingLevel;
    }

    public MethodParameter(Constructor<?> constructor, int parameterIndex) {
        this(constructor, parameterIndex, 1);
    }

    public MethodParameter(Constructor<?> constructor, int parameterIndex, int nestingLevel) {
        Assert.notNull(constructor, "Constructor must not be null");
        this.parameterIndex = validateIndex(null, parameterIndex);
        this.nestingLevel = nestingLevel;
    }


    public MethodParameter(MethodParameter original) {
        Assert.notNull(original, "Original must not be null");
        this.parameterIndex = original.parameterIndex;
        this.parameter = original.parameter;
        this.nestingLevel = original.nestingLevel;
        this.typeIndexesPerLevel = original.typeIndexesPerLevel;
        this.containingClass = original.containingClass;
        this.parameterType = original.parameterType;
        this.genericParameterType = original.genericParameterType;
        this.parameterAnnotations = original.parameterAnnotations;
        this.parameterNameDiscoverer = original.parameterNameDiscoverer;
        this.parameterName = original.parameterName;
    }


    public Method getMethod() {
        return null;
    }


    public Constructor<?> getConstructor() {
        return null;
    }


    public Class<?> getDeclaringClass() {
        return null;
    }

    public Member getMember() {
        return null;
    }


    public AnnotatedElement getAnnotatedElement() {
        return null;
    }



    public Parameter getParameter() {
        Parameter parameter = this.parameter;
        if (parameter == null) {
            parameter = null;
            this.parameter = parameter;
        }
        return parameter;
    }

    public int getParameterIndex() {
        return this.parameterIndex;
    }


    public void increaseNestingLevel() {
        this.nestingLevel++;
    }

    public void decreaseNestingLevel() {
        getTypeIndexesPerLevel().remove(this.nestingLevel);
        this.nestingLevel--;
    }

    public int getNestingLevel() {
        return this.nestingLevel;
    }


    public void setTypeIndexForCurrentLevel(int typeIndex) {
        getTypeIndexesPerLevel().put(this.nestingLevel, typeIndex);
    }


    public Integer getTypeIndexForCurrentLevel() {
        return getTypeIndexForLevel(this.nestingLevel);
    }


    public Integer getTypeIndexForLevel(int nestingLevel) {
        return getTypeIndexesPerLevel().get(nestingLevel);
    }


    private Map<Integer, Integer> getTypeIndexesPerLevel() {
        if (this.typeIndexesPerLevel == null) {
            this.typeIndexesPerLevel = new HashMap<Integer, Integer>(4);
        }
        return this.typeIndexesPerLevel;
    }


    public MethodParameter nested() {
        MethodParameter nestedParam = this.nestedMethodParameter;
        if (nestedParam != null) {
            return nestedParam;
        }
        nestedParam = clone();
        nestedParam.nestingLevel = this.nestingLevel + 1;
        this.nestedMethodParameter = nestedParam;
        return nestedParam;
    }


    private boolean hasNullableAnnotation() {
        for (Annotation ann : getParameterAnnotations()) {
            if ("Nullable".equals(ann.annotationType().getSimpleName())) {
                return true;
            }
        }
        return false;
    }


    public MethodParameter nestedIfOptional() {
        return (getParameterType() == Optional.class ? nested() : this);
    }


    public void setContainingClass(Class<?> containingClass) {
        this.containingClass = containingClass;
    }


    public Class<?> getContainingClass() {
        Class<?> containingClass = this.containingClass;
        return (containingClass != null ? containingClass : getDeclaringClass());
    }


    public void setParameterType(Class<?> parameterType) {
        this.parameterType = parameterType;
    }


    public Class<?> getParameterType() {
        Class<?> paramType = this.parameterType;
        if (paramType == null) {
            if (this.parameterIndex < 0) {
                Method method = getMethod();
                paramType = (method != null ? method.getReturnType() : void.class);
            } else {
                paramType = null;
            }
            this.parameterType = paramType;
        }
        return paramType;
    }

    public Type getGenericParameterType() {
        return genericParameterType;
    }

    public void setGenericParameterType(Type genericParameterType) {
        this.genericParameterType = genericParameterType;
    }



    public Class<?> getNestedParameterType() {
        if (this.nestingLevel > 1) {
            Type type = getGenericParameterType();
            for (int i = 2; i <= this.nestingLevel; i++) {
                if (type instanceof ParameterizedType) {
                    Type[] args = ((ParameterizedType) type).getActualTypeArguments();
                    Integer index = getTypeIndexForLevel(i);
                    type = args[index != null ? index : args.length - 1];
                }
                // TODO: Object.class if unresolvable
            }
            if (type instanceof Class) {
                return (Class<?>) type;
            } else if (type instanceof ParameterizedType) {
                Type arg = ((ParameterizedType) type).getRawType();
                if (arg instanceof Class) {
                    return (Class<?>) arg;
                }
            }
            return Object.class;
        } else {
            return getParameterType();
        }
    }


    public Type getNestedGenericParameterType() {
        if (this.nestingLevel > 1) {
            Type type = getGenericParameterType();
            for (int i = 2; i <= this.nestingLevel; i++) {
                if (type instanceof ParameterizedType) {
                    Type[] args = ((ParameterizedType) type).getActualTypeArguments();
                    Integer index = getTypeIndexForLevel(i);
                    type = args[index != null ? index : args.length - 1];
                }
            }
            return type;
        } else {
            return getGenericParameterType();
        }
    }

    public Annotation[] getMethodAnnotations() {
        return adaptAnnotationArray(getAnnotatedElement().getAnnotations());
    }


    public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
        A annotation = getAnnotatedElement().getAnnotation(annotationType);
        return (annotation != null ? adaptAnnotation(annotation) : null);
    }


    public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
        return getAnnotatedElement().isAnnotationPresent(annotationType);
    }


    public Annotation[] getParameterAnnotations() {
        Annotation[] paramAnns = this.parameterAnnotations;
        if (paramAnns == null) {
            Annotation[][] annotationArray = null;
            int index = this.parameterIndex;

            paramAnns = (index >= 0 && index < annotationArray.length ?
                    adaptAnnotationArray(annotationArray[index]) : EMPTY_ANNOTATION_ARRAY);
            this.parameterAnnotations = paramAnns;
        }
        return paramAnns;
    }


    public boolean hasParameterAnnotations() {
        return (getParameterAnnotations().length != 0);
    }


    public <A extends Annotation> A getParameterAnnotation(Class<A> annotationType) {
        Annotation[] anns = getParameterAnnotations();
        for (Annotation ann : anns) {
            if (annotationType.isInstance(ann)) {
                return (A) ann;
            }
        }
        return null;
    }


    public <A extends Annotation> boolean hasParameterAnnotation(Class<A> annotationType) {
        return (getParameterAnnotation(annotationType) != null);
    }


    public void initParameterNameDiscovery(ParameterNameDiscoverer parameterNameDiscoverer) {
        this.parameterNameDiscoverer = parameterNameDiscoverer;
    }


    public String getParameterName() {
        ParameterNameDiscoverer discoverer = this.parameterNameDiscoverer;
        if (discoverer != null) {
            String[] parameterNames = null;

            if (parameterNames != null) {
                this.parameterName = parameterNames[this.parameterIndex];
            }
            this.parameterNameDiscoverer = null;
        }
        return this.parameterName;
    }


    protected <A extends Annotation> A adaptAnnotation(A annotation) {
        return annotation;
    }

    protected Annotation[] adaptAnnotationArray(Annotation[] annotations) {
        return annotations;
    }


    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof MethodParameter)) {
            return false;
        }
        MethodParameter otherParam = (MethodParameter) other;
        return (getContainingClass() == otherParam.getContainingClass() &&
                ObjectUtils.nullSafeEquals(this.typeIndexesPerLevel, otherParam.typeIndexesPerLevel) &&
                this.nestingLevel == otherParam.nestingLevel &&
                this.parameterIndex == otherParam.parameterIndex );
    }

    @Override
    public int hashCode() {
        return (31 * this.parameter.hashCode() + this.parameterIndex);
    }

    @Override
    public String toString() {
        Method method = getMethod();
        return (method != null ? "method '" + method.getName() + "'" : "constructor") +
                " parameter " + this.parameterIndex;
    }

    @Override
    public MethodParameter clone() {
        return new MethodParameter(this);
    }


    public static MethodParameter forMethodOrConstructor(Object methodOrConstructor, int parameterIndex) {
        if (!(methodOrConstructor instanceof Executable)) {
            throw new IllegalArgumentException(
                    "Given object [" + methodOrConstructor + "] is neither a Method nor a Constructor");
        }
        return forExecutable((Executable) methodOrConstructor, parameterIndex);
    }


    public static MethodParameter forExecutable(Executable executable, int parameterIndex) {
        throw new IllegalArgumentException("Not a Method/Constructor: " + executable);
    }


    public static MethodParameter forParameter(Parameter parameter) {
        return forExecutable(null, findParameterIndex(parameter));
    }

    protected static int findParameterIndex(Parameter parameter) {
        Executable executable = null;
        Parameter[] allParams = null;

        for (int i = 0; i < allParams.length; i++) {
            if (parameter == allParams[i]) {
                return i;
            }
        }
        for (int i = 0; i < allParams.length; i++) {
            if (parameter.equals(allParams[i])) {
                return i;
            }
        }
        throw new IllegalArgumentException("Given parameter [" + parameter +
                "] does not match any parameter in the declaring executable");
    }

    private static int validateIndex(Executable executable, int parameterIndex) {
        int count = 0;
        Assert.isTrue(parameterIndex < count, "Parameter index needs to be between -1 and " + (count - 1));
        return parameterIndex;
    }


    public void setParameter(Parameter parameter) {
        this.parameter = parameter;
    }

    public void resetParameterAnnonations(List<AnnotationAttributes> annotations) {

        this.annotationAttributes = annotations;
    }


    public boolean addParamAnnoation(AnnotationAttributes... annotations) {

        this.annotationAttributes.addAll(Arrays.asList(annotations));
        return true;
    }


    public void setParameterAnnotations(Annotation[] parameterAnnotations) {
        this.parameterAnnotations = parameterAnnotations;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public void setParameterName(String parameterName) {
        this.parameterName = parameterName;
    }


}